#!/usr/bin/env python

# (c) 2007-2011 Helsinki University of Technology and University of Helsinki
# Licensed under the MIT license; see LICENSE.txt for more information.

from harava.worker import Worker
from harava.rdf import *
from harava.validate import Validator

import re
import sys


class TSPublicationValidator (Validator):
  def __init__(self, session):
    Validator.__init__(self, session)
    self._seen_identifiers = {}

    self._additionalMetadataModels = [session.getModelService().getModel(model) 
                                      for model in session.getSource().getAdditionalMetadataModels()]

  def guess_genre(self, doc):
    url = doc.getURL()
    if 'opas/' in url or 'oppaat' in url or 'kasikirja' in url or 'ohje/' in url or 'ohjeet' in url or 'matkailijan_terveys' in url or 'suositu' in url:
      return GENRE.guide
    if 'tutkii' in url or 'julkaisusarjat' in url:
      return GENRE.research
    if 'elama_pelissa' in url or 'testit' in url:
      return GENRE.test
    if 'lehti' in url:
      return GENRE.article
    if 'esittely' in url or 'osastot' in url:
      return GENRE.organizational
    if 'tietoa_terveydesta' in url:
      return GENRE.article
    
    return GENRE.article

  def copy_metadata(self, rdf, doc, subj, seen=None):
    if seen is None:
      seen = set()
    seen.add(subj)
    for pred, obj in rdf.predicate_objects(subj):
      doc.setMetadataTriple(subj, pred, obj)
      if (isinstance(obj, URIRef) or isinstance(obj, BNode)) and obj not in seen: # copy recursively
        self.copy_metadata(rdf, doc, obj, seen)

  def process(self, doc):
    # check required fields
    success = True
    
    # dc:identifier
    vals = doc.getMetadata(DC.identifier)
    if not vals:
      doc.log('NOTE', 'validate', 'Missing required field dc:identifier; using URL')
      ident = URIRef(doc.getURL())
      doc.setMetadata(DC.identifier, ident)
    elif len(vals) > 1:
      ident = list(vals)[0]
      for val in vals:
        if len(val) < len(ident):
          ident = val
      doc.log('WARN', 'validate', 'Multiple dc:identifier values given, choosing shortest', ident)
      doc.clearMetadata(DC.identifier)
      doc.setMetadata(DC.identifier, ident)
    else:
      ident = list(vals)[0]

    if self._seen_identifiers.has_key(ident):
      doc.log('SKIP', 'validate', 'Duplicate dc:identifier, using metadata from original URL', re.sub(r'(\S{40})', r'\1 ', self._seen_identifiers[ident]))
      success = False
    else:
      self._seen_identifiers[ident] = doc.getURL()

    # add metadata from additional metadata models
    for model in self._additionalMetadataModels:
      self.copy_metadata(model, doc, ident)

      
    # dc:language
    vals = doc.getMetadata(DC.language)

    newvals = []
    for lang in vals:
      if str(lang) not in ('fi', 'sv', 'en'): # TODO make configurable
        doc.log('WARN', 'validate', 'Unsupported language', lang)
      else:
        newvals.append(lang)
    if len(newvals) < len(vals):
      doc.clearMetadata(DC.language)
      for lang in newvals:
        doc.setMetadata(DC.language, lang)
      vals = newvals

    if not vals:
      # TODO make this configurable (default value)
      doc.log('CRIT', 'validate', 'Missing required field', 'dc:language')
      success = False
      defaultLanguage = None
    elif len(vals) > 1:
      doc.log('WARN', 'validate', 'Multiple languages defined, picking one')
      defaultLanguage = list(vals)[0]
      doc.clearMetadata(DC.language)
      doc.setMetadata(DC.language, defaultLanguage)
    else:
      defaultLanguage = list(vals)[0]
    
    # check datatype
    if defaultLanguage and defaultLanguage.datatype != DCTERMS.RFC4646:
      doc.log('NOTE', 'validate', 'Invalid datatype for dc:language value', defaultLanguage.datatype)
      doc.clearMetadata(DC.language)
      doc.setMetadata(DC.language, Literal(str(defaultLanguage), datatype=DCTERMS.RFC4646))

    # dc:title
    vals = doc.getMetadata(DC['title'])
    if not vals:
      # try to use <title> tag
      found = False
      tree = doc.getParseTree()
      if tree is not None:
        titletags = list(tree.getiterator('title'))
        if titletags:
          try:
            title = titletags[0].text
          except Exception, e:
            doc.log('WARN', 'validate', 'Exception caught while parsing <title> element', str(e))
            title = ''
          if title != '':
            doc.log('WARN', 'validate', 'Missing required field dc:title, using value from <title> element')
            doc.setMetadata(DC['title'], Literal(title))
            found = True

      if not found:
        doc.log('CRIT', 'validate', 'Missing required field', 'dc:title')
        success = False
    
    # dc:description
    vals = doc.getMetadata(DC.description)
    if not vals:
      doc.log('WARN', 'validate', 'Missing required field dc:description, using empty value')
      doc.setMetadata(DC.description, Literal(''))
    
    # dcterms:issued
    # TODO validate date value
    vals = doc.getMetadata(DCTERMS.issued)
    if not vals:
      # try to find another value from dcterms:modified and dcterms:dateAccepted fields
      modified = doc.getMetadata(DCTERMS.modified, single=True)
      accepted = doc.getMetadata(DCTERMS.dateAccepted, single=True)
      if modified:
        doc.log('WARN', 'validate', 'Missing required field dcterms:issued, using dcterms:modified value')
        doc.setMetadata(DCTERMS.issued, modified)
      elif accepted:
        doc.log('WARN', 'validate', 'Missing required field dcterms:issued, using dcterms:dateAccepted value')
        doc.setMetadata(DCTERMS.issued, accepted)
      else:
        # use a default value
        # TODO make this configurable
        doc.setMetadata(DCTERMS.issued, Literal('2000', datatype=DCTERMS.W3CDTF))
        doc.log('WARN', 'validate', 'Missing required field dcterms:issued, using default value')
    
    # dc:publisher
    vals = doc.getMetadata(DC.publisher)
    if not vals:
      defaultPublisher = self._session.getSource().getDefaultPublisher()
      if defaultPublisher:
        doc.log('NOTE', 'validate', 'Missing required field dc:publisher, using default')
        doc.setMetadata(DC.publisher, URIRef(defaultPublisher))
      else:
        doc.log('CRIT', 'validate', 'Missing required field', 'dc:publisher')
        success = False
    
    # ts:genre
    vals = doc.getMetadata(TS.genre)
    if not vals:
      defgen = self._session.getSource().getDefaultGenre()
      if defgen:
        genre = URIRef(defgen)
        doc.log('NOTE', 'validate', 'Missing required field ts:genre, using default')
        doc.setMetadata(TS.genre, genre)
      else:
        # TODO make this configurable
        genre = self.guess_genre(doc)
        doc.log('NOTE', 'validate', 'Missing required field ts:genre, using guessed value ' + genre)
        doc.setMetadata(TS.genre, genre)
    elif len(vals) > 1:
      doc.log('WARN', 'validate', 'Multiple genres defined, picking one')
      genre = list(vals)[0]
      doc.clearMetadata(TS.genre)
      doc.setMetadata(TS.genre, genre)
    else:
      genre = list(vals)[0]

    # validate the genre value
    if not self.validate_concept(genre, SKOS.Concept, GENRE.conceptscheme):
      doc.log('CRIT', 'validate', 'Invalid genre value', genre)
      success = False
    
    # dc:type
    vals = doc.getMetadata(DC.type)
    if not vals:
      # TODO make this configurable
      doc.log('NOTE', 'validate', 'Missing required field dc:type, using dcmitype:Text')
      doc.setMetadata(DC.type, DCMITYPE.Text)
    
    # dc:format
    vals = doc.getMetadata(DC['format'])

    newvals = []
    for fmt in vals:
      if str(fmt) not in ('text/html', 'application/pdf'):
        doc.log('WARN', 'validate', 'Unsupported MIME type', fmt)
      else:
        newvals.append(fmt)
    if len(newvals) < len(vals):
      doc.clearMetadata(DC['format'])
      for fmt in newvals:
        doc.setMetadata(DC['format'], fmt)
      vals = newvals

    if not vals:
      # TODO make this configurable
      doc.log('NOTE', 'validate', 'Missing required field dc:format, using "text/html"')
      doc.setMetadata(DC['format'], Literal('text/html', datatype=DCTERMS.IMT))
    elif len(vals) > 1:
      doc.log('WARN', 'validate', 'Multiple formats defined, picking one')
      fmt = list(vals)[0]
      doc.clearMetadata(DC['format'])
      doc.setMetadata(DC['format'], fmt)

    # check datatype
    fmt = list(doc.getMetadata(DC['format']))[0]
    if fmt.datatype != DCTERMS.IMT:
      doc.log('NOTE', 'validate', 'Invalid datatype for dc:format value', fmt.datatype)
      doc.clearMetadata(DC['format'])
      doc.setMetadata(DC['format'], Literal(str(fmt), datatype=DCTERMS.IMT))

    
    # dc:subject
    vals = doc.getMetadata(DC.subject)
    if not vals:
      doc.log('CRIT', 'validate', 'Missing required field', 'dc:subject')
      success = False
    elif len(vals) < 3:
      doc.log('WARN', 'validate', 'Fewer than 3 subjects defined')
    elif len(vals) > 15:
      doc.log('WARN', 'validate', 'More than 15 subjects defined')
    
    # dcterms:audience
    vals = doc.getMetadata(DCTERMS.audience)
    if not vals:
      defaud = self._session.getSource().getDefaultAudience()
      if defaud:
        audience = URIRef(defaud)
        doc.log('NOTE', 'validate', 'Missing required field dcterms:audience, using default')
        doc.setMetadata(DCTERMS.audience, audience)
      else:
        audience = None
        doc.log('CRIT', 'validate', 'Missing required field dcterms:audience and no default defined')
        success = False
    elif len(vals) > 1:
      doc.log('WARN', 'validate', 'Multiple audiences defined, picking one')
      audience = list(vals)[0]
      doc.clearMetadata(DCTERMS.audience)
      doc.setMetadata(DCTERMS.audience, audience)
    else:
      audience = list(vals)[0]
    
    # validate the audience value
    if not self.validate_concept(audience, SKOS.Concept, SECTION.conceptscheme):
      doc.log('CRIT', 'validate', 'Invalid audience value', audience)
      success = False
    
    if not success:
      doc.processingFinished()
      return False
    
    # set default languages for dc:title, dc:description, dc:rights and ts:keyword
    # TODO what about foaf:name?
    for prop in (DC['title'], DC.description, DC.rights, TS.keyword):
      vals = list(doc.getMetadata(prop))
      for val in vals:
        if not val.language:
          doc.clearMetadata(prop, val)
          doc.setMetadata(prop, Literal(val, defaultLanguage))
    
    vals = doc.getMetadata(TS.filename)
    if vals: # clearly a local publication
      # say it's a local publication and clear the URL
      doc.setMetadata(RDF.type, TS.LocalPublication)
      doc.clearMetadata(TS.url)
    else:
      # if it's an external publication, make sure to declare ts:url
      if doc.getMetadata(TS.url): # clearly an external publication
        doc.setMetadata(RDF.type, TS.ExternalPublication)
      else:
        # remaining case: external(?) without url
        if list(doc.getMetadata(DC.identifier))[0].startswith('http://www.tervesuomi.fi'):
          # document from www.tervesuomi.fi, must be local
          doc.setMetadata(RDF.type, TS.LocalPublication)
        else:
          # external document, set the URL from the identifier
          doc.setMetadata(RDF.type, TS.ExternalPublication)
          doc.setMetadata(TS.url, list(doc.getMetadata(DC.identifier))[0])
    
    
    doc.setCompleted('validate')
    self._session.schedule(doc)
    return True

  def finish(self):
    # possibly free some memory
    del self._additionalMetadataModels
    Validator.finish(self)
